﻿/*
 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
 For licensing, see LICENSE.html or http://ckeditor.com/license
 */

/**
 * Creates a {@link CKEDITOR.htmlParser} class instance.
 * @class Provides an "event like" system to parse strings of HTML data.
 * @example
 * var parser = new CKEDITOR.htmlParser();
 * parser.onTagOpen = function( tagName, attributes, selfClosing )
 *     {
 *         alert( tagName );
 *     };
 * parser.parse( '&lt;p&gt;Some &lt;b&gt;text&lt;/b&gt;.&lt;/p&gt;' );
 */
CKEDITOR.htmlParser = function () {
    this._ =
    {
        htmlPartsRegex: new RegExp('<(?:(?:\\/([^>]+)>)|(?:!--([\\S|\\s]*?)-->)|(?:([^\\s>]+)\\s*((?:(?:"[^"]*")|(?:\'[^\']*\')|[^"\'>])*)\\/?>))', 'g')
    };
};

(function () {
    var attribsRegex = /([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g,
        emptyAttribs = {
            checked: 1,
            compact: 1,
            declare: 1,
            defer: 1,
            disabled: 1,
            ismap: 1,
            multiple: 1,
            nohref: 1,
            noresize: 1,
            noshade: 1,
            nowrap: 1,
            readonly: 1,
            selected: 1
        };

    CKEDITOR.htmlParser.prototype =
    {
        /**
         * Function to be fired when a tag opener is found. This function
         * should be overriden when using this class.
         * @param {String} tagName The tag name. The name is guarantted to be
         *        lowercased.
         * @param {Object} attributes An object containing all tag attributes. Each
         *        property in this object represent and attribute name and its
         *        value is the attribute value.
         * @param {Boolean} selfClosing true if the tag closes itself, false if the
         *        tag doesn't.
         * @example
         * var parser = new CKEDITOR.htmlParser();
         * parser.onTagOpen = function( tagName, attributes, selfClosing )
         *     {
		 *         alert( tagName );  // e.g. "b"
		 *     });
         * parser.parse( "&lt;!-- Example --&gt;&lt;b&gt;Hello&lt;/b&gt;" );
         */
        onTagOpen: function () {
        },

        /**
         * Function to be fired when a tag closer is found. This function
         * should be overriden when using this class.
         * @param {String} tagName The tag name. The name is guarantted to be
         *        lowercased.
         * @example
         * var parser = new CKEDITOR.htmlParser();
         * parser.onTagClose = function( tagName )
         *     {
		 *         alert( tagName );  // e.g. "b"
		 *     });
         * parser.parse( "&lt;!-- Example --&gt;&lt;b&gt;Hello&lt;/b&gt;" );
         */
        onTagClose: function () {
        },

        /**
         * Function to be fired when text is found. This function
         * should be overriden when using this class.
         * @param {String} text The text found.
         * @example
         * var parser = new CKEDITOR.htmlParser();
         * parser.onText = function( text )
         *     {
		 *         alert( text );  // e.g. "Hello"
		 *     });
         * parser.parse( "&lt;!-- Example --&gt;&lt;b&gt;Hello&lt;/b&gt;" );
         */
        onText: function () {
        },

        /**
         * Function to be fired when CDATA section is found. This function
         * should be overriden when using this class.
         * @param {String} cdata The CDATA been found.
         * @example
         * var parser = new CKEDITOR.htmlParser();
         * parser.onCDATA = function( cdata )
         *     {
		 *         alert( cdata );  // e.g. "var hello;"
		 *     });
         * parser.parse( "&lt;script&gt;var hello;&lt;/script&gt;" );
         */
        onCDATA: function () {
        },

        /**
         * Function to be fired when a commend is found. This function
         * should be overriden when using this class.
         * @param {String} comment The comment text.
         * @example
         * var parser = new CKEDITOR.htmlParser();
         * parser.onComment = function( comment )
         *     {
		 *         alert( comment );  // e.g. " Example "
		 *     });
         * parser.parse( "&lt;!-- Example --&gt;&lt;b&gt;Hello&lt;/b&gt;" );
         */
        onComment: function () {
        },

        /**
         * Parses text, looking for HTML tokens, like tag openers or closers,
         * or comments. This function fires the onTagOpen, onTagClose, onText
         * and onComment function during its execution.
         * @param {String} html The HTML to be parsed.
         * @example
         * var parser = new CKEDITOR.htmlParser();
         * // The onTagOpen, onTagClose, onText and onComment should be overriden
         * // at this point.
         * parser.parse( "&lt;!-- Example --&gt;&lt;b&gt;Hello&lt;/b&gt;" );
         */
        parse: function (html) {
            var parts,
                tagName,
                nextIndex = 0,
                cdata;	// The collected data inside a CDATA section.

            while (( parts = this._.htmlPartsRegex.exec(html) )) {
                var tagIndex = parts.index;
                if (tagIndex > nextIndex) {
                    var text = html.substring(nextIndex, tagIndex);

                    if (cdata)
                        cdata.push(text);
                    else
                        this.onText(text);
                }

                nextIndex = this._.htmlPartsRegex.lastIndex;

                /*
                 "parts" is an array with the following items:
                 0 : The entire match for opening/closing tags and comments.
                 1 : Group filled with the tag name for closing tags.
                 2 : Group filled with the comment text.
                 3 : Group filled with the tag name for opening tags.
                 4 : Group filled with the attributes part of opening tags.
                 */

                // Closing tag
                if (( tagName = parts[1] )) {
                    tagName = tagName.toLowerCase();

                    if (cdata && CKEDITOR.dtd.$cdata[tagName]) {
                        // Send the CDATA data.
                        this.onCDATA(cdata.join(''));
                        cdata = null;
                    }

                    if (!cdata) {
                        this.onTagClose(tagName);
                        continue;
                    }
                }

                // If CDATA is enabled, just save the raw match.
                if (cdata) {
                    cdata.push(parts[0]);
                    continue;
                }

                // Opening tag
                if (( tagName = parts[3] )) {
                    tagName = tagName.toLowerCase();

                    // There are some tag names that can break things, so let's
                    // simply ignore them when parsing. (#5224)
                    if (/="/.test(tagName))
                        continue;

                    var attribs = {},
                        attribMatch,
                        attribsPart = parts[4],
                        selfClosing = !!( attribsPart && attribsPart.charAt(attribsPart.length - 1) == '/' );

                    if (attribsPart) {
                        while (( attribMatch = attribsRegex.exec(attribsPart) )) {
                            var attName = attribMatch[1].toLowerCase(),
                                attValue = attribMatch[2] || attribMatch[3] || attribMatch[4] || '';

                            if (!attValue && emptyAttribs[attName])
                                attribs[attName] = attName;
                            else
                                attribs[attName] = attValue;
                        }
                    }

                    this.onTagOpen(tagName, attribs, selfClosing);

                    // Open CDATA mode when finding the appropriate tags.
                    if (!cdata && CKEDITOR.dtd.$cdata[tagName])
                        cdata = [];

                    continue;
                }

                // Comment
                if (( tagName = parts[2] ))
                    this.onComment(tagName);
            }

            if (html.length > nextIndex)
                this.onText(html.substring(nextIndex, html.length));
        }
    };
})();
